/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch.fileLevel;

import static milk.jpatch.Util.logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;

import milk.jpatch.CPoolMap;
import milk.jpatch.Patch;
import milk.jpatch.Util;
import milk.jpatch.access.AccessFlagsPatch;
import milk.jpatch.attribs.AttributesPatch;
import milk.jpatch.classLevel.FieldsPatch;
import milk.jpatch.classLevel.MethodsPatch;

import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Attribute;

/**
 * Patch for a java .class file.
 * @author Thomas Kerber
 * @version 1.0.3
 */
// TODO: Check if this deserialized properly now.
public class ClassPatch extends FilePatch implements Patch<JavaClass>{
    
    private static final long serialVersionUID = -8012147988738144393L;
    
    /**
     * The postfix of the classpatch file (i.e. file extension)
     */
    public static final String POSTFIX = ".milk.jpatch.jcp";
    
    // TODO: maybe issue warnings when these don't match or summit *shrug*
    /**
     * The new files major version.
     */
    public final int majorVersion;
    /**
     * The new files minor version.
     */
    public final int minorVersion;
    
    /**
     * The new cpool.
     */
    private final ConstantPool cpool;
    /**
     * The component flags patch.
     */
    private final AccessFlagsPatch accessPatch;
    /**
     * The superclass patch. Null means ID patch, anything else a replacement
     * patch.
     */
    public final String superClass;
    /**
     * Interfaces which were added.
     */
    private final String[] interfacesAdded;
    /**
     * Interfaces which were removed.
     */
    private final String[] interfacesRemoved;
    /**
     * The component fields patch.
     */
    private final FieldsPatch fieldsPatch;
    /**
     * The component methods patch.
     */
    private final MethodsPatch methodsPatch;
    /**
     * The component attributes patch.
     */
    private final AttributesPatch attributesPatch;
    
    /**
     * Creates.
     * @param name The file name.
     * @param majorVersion The classes major version.
     * @param minorVersion The classes minor version.
     * @param cpool The constant pool to add.
     * @param accessPatch The component access patch.
     * @param superClass The superclass patch. Null means ID patch, anything
     *     a replacement patch.
     * @param interfacesAdded The interface names added.
     * @param interfacesRemoved The interface names removed.
     * @param fieldsPatch The component fields patch.
     * @param methodsPatch The component methods patch.
     * @param attributesPatch The component attributes patch.
     */
    public ClassPatch(
            String name,
            int majorVersion,
            int minorVersion,
            ConstantPool cpool,
            AccessFlagsPatch accessPatch,
            String superClass,
            String[] interfacesAdded,
            String[] interfacesRemoved,
            FieldsPatch fieldsPatch,
            MethodsPatch methodsPatch,
            AttributesPatch attributesPatch){
        super(name);
        this.majorVersion = majorVersion;
        this.minorVersion = minorVersion;
        this.cpool = cpool;
        this.accessPatch = accessPatch;
        this.superClass = superClass;
        this.interfacesAdded = interfacesAdded;
        this.interfacesRemoved = interfacesRemoved;
        this.fieldsPatch = fieldsPatch;
        this.methodsPatch = methodsPatch;
        this.attributesPatch = attributesPatch;
    }
    
    /**
     * Creates.
     * @param name The file name.
     * @param old The old class.
     * @param modif The modified class.
     */
    public ClassPatch(String name, JavaClass old, JavaClass modif){
        super(name);
        this.majorVersion = modif.getMajor();
        this.minorVersion = modif.getMinor();
        this.cpool = modif.getConstantPool();
        this.accessPatch = AccessFlagsPatch.generate(old.getAccessFlags(),
                modif.getAccessFlags(), AccessFlagsPatch.AccessLevel.CLASS);
        
        if(old.getSuperclassName().equals(modif.getSuperclassName()))
            this.superClass = null;
        else
            this.superClass = modif.getSuperclassName();
        
        String[] oldInterfaces = Util.getInterfaceNamesProper(old);
        String[] modInterfaces = Util.getInterfaceNamesProper(modif);
        
        List<String> addedInterfacesL = new ArrayList<String>();
        List<String> removedInterfacesL = new ArrayList<String>();
        
        outer: for(String oInt : oldInterfaces){
            for(String mInt : modInterfaces){
                if(oInt.equals(mInt))
                    continue outer;
            }
            removedInterfacesL.add(oInt);
        }
        outer: for(String mInt : modInterfaces){
            for(String oInt : oldInterfaces){
                if(oInt.equals(mInt))
                    continue outer;
            }
            addedInterfacesL.add(mInt);
        }
        
        this.interfacesAdded = addedInterfacesL.toArray(
                new String[addedInterfacesL.size()]);
        this.interfacesRemoved = removedInterfacesL.toArray(
                new String[removedInterfacesL.size()]);
        
        // TODO: Switch these around, and use the new cpool instead. 
        CPoolMap map = CPoolMap.generate(old.getConstantPool(),
            modif.getConstantPool());
        
        this.fieldsPatch =
                FieldsPatch.generate(old.getFields(), modif.getFields(), map);
        this.methodsPatch =
                MethodsPatch.generate(old.getMethods(), modif.getMethods(),
                        map);
        this.attributesPatch = AttributesPatch.generate(old.getAttributes(),
                modif.getAttributes(), map);
    } 
    
    /**
     * Generates.
     * @param orig The original file.
     * @param mod The modded file.
     * @param name The file name.
     * @return The patch.
     * @throws IOException If files cannot be read or parsed.
     */
    public static ClassPatch generate(File orig, File mod, String name)
            throws IOException{
        return new ClassPatch(
                name,
                new ClassParser(orig.getAbsolutePath()).parse(),
                new ClassParser(orig.getAbsolutePath()).parse());
    }
    
    /**
     * 
     * @return The patches constant pool.
     */
    public ConstantPool getCPool(){
        return cpool;
    }
    
    /**
     * 
     * @return The component flags patch.
     */
    public AccessFlagsPatch getFlagsPatch(){
        return accessPatch;
    }
    
    /**
     * 
     * @return The interfaces added by the patch.
     */
    public String[] getAddedInterfaces(){
        return interfacesAdded;
    }
    
    /**
     * 
     * @return The interfaces removed by the patch.
     */
    public String[] getRemovedInterfaces(){
        return interfacesRemoved;
    }
    
    /**
     * 
     * @return The component fields patch.
     */
    public FieldsPatch getFieldsPatch(){
        return fieldsPatch;
    }
    
    /**
     * 
     * @return The component methods patch.
     */
    public MethodsPatch getMethodsPatch(){
        return methodsPatch;
    }
    
    /**
     * 
     * @return The component attributes patch.
     */
    public AttributesPatch getAttributesPatch(){
        return attributesPatch;
    }
    
    /**
     * Dumps as .jcp
     * @param file The file to dump to.
     * @throws IOException If an output error occured.
     */
    public void dump(String file) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(file));
        oos.writeObject(this);
        oos.close();
    }
    
    @Override
    public JavaClass patch(JavaClass j, CPoolMap map){
        
        int flags = accessPatch.patch(j.getAccessFlags());
        
        ConstantPool cpool = map.to;
        
        int superclassIndex;
        if(superClass == null)
            superclassIndex = j.getSuperclassNameIndex();
        else
            superclassIndex = Util.findConstantClassIn(map, superClass);
        
        List<String> interfaceNames = new ArrayList<String>(
                new ArrayList<String>(Arrays.asList(
                        Util.getInterfaceNamesProper(j))));
        for(String intRem : interfacesRemoved)
            interfaceNames.remove(intRem);
        for(String intAdd : interfacesAdded)
            if(!interfaceNames.contains(intAdd))
                interfaceNames.add(intAdd);
        int[] interfaces = new int[interfaceNames.size()];
        for(int i = 0; i < interfaces.length; i++)
            interfaces[i] = Util.findConstantClassIn(map,
                    interfaceNames.get(i));
        
        List<Field> fieldList = new ArrayList<Field>(
                Arrays.asList(j.getFields()));
        fieldList = fieldsPatch.patch(fieldList, map);
        Field[] fields = fieldList.toArray(new Field[fieldList.size()]);
        
        List<Method> methodList = new ArrayList<Method>(
                Arrays.asList(j.getMethods()));
        methodList = methodsPatch.patch(methodList, map);
        Method[] methods = methodList.toArray(new Method[methodList.size()]);
        
        List<Attribute> attributeList = new ArrayList<Attribute>(
                Arrays.asList(j.getAttributes()));
        attributeList = attributesPatch.patch(attributeList, map);
        Attribute[] attributes = attributeList.toArray(
                new Attribute[attributeList.size()]);
        
        int classIndex = j.getClassNameIndex();
        
        return new JavaClass(
                classIndex,
                superclassIndex,
                "[-]",
                majorVersion,
                minorVersion,
                flags,
                cpool,
                interfaces,
                fields,
                methods,
                attributes);
    }
    
    /**
     * Patches a class.
     * @param j The class to patch.
     * @return The patched class.
     */
    public JavaClass patch(JavaClass j){
        return patch(j, CPoolMap.generate(j.getConstantPool(), cpool));
    }
    
    @Override
    public void patch(InputStream in, OutputStream out) throws IOException{
        /*
         * If a class format exception, or *ANY* other error during the patch
         * (save IOException) occurs, the old file is copied over instead, and
         * the program CONTINUES EXECUTING! A severe log message is entered.
         */
        try{
            patch(new ClassParser(in, this.name).parse()).dump(out);
            in.close();
            out.close();
        }
        catch(IOException e){
            // IOExceptions simply get re-risen
            throw e;
        }
        catch(ClassFormatException e){
            logger.log(Level.SEVERE,
                    "Patch of '" + name + "' FAILED. The file " +
                    "will NOT BE PATCHED. The file could not be parsed.",
                    e);
            Util.loop(in, out);
        }
        catch(Exception e){
            logger.log(Level.SEVERE,
                    "Patch of '" + name + "' FAILED. The file will " +
                    "NOT BE PATCHED. A patch could not be created, please " +
                    "file a bug report with this log info.",
                    e);
            Util.loop(in, out);
        }
    }
    
    @Override
    public void serialize(File root) throws IOException{
        File outF = new File(root, name + POSTFIX);
        outF.getParentFile().mkdirs();
        ObjectOutputStream out = new ObjectOutputStream(
                new FileOutputStream(outF));
        out.writeObject(this);
        out.close();
    }
    
    /**
     * Checks if deserialization is possible from a file.
     * @param f The file to check.
     * @return Whether it can be deserialized.
     */
    public static boolean canDeserializeAt(File f){
        return f.getName().endsWith(POSTFIX);
    }
    
    /**
     * Deserialized a file.
     * @param root The root relate to.
     * @param f The file to deserialize.
     * @return The patch.
     * @throws IOException If a read or parse error occurred.
     */
    public static ClassPatch deserializeAt(File root, File f)
            throws IOException{
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(f));
        ClassPatch cp;
        try{
            cp = (ClassPatch)in.readObject();
        }
        catch(ClassCastException e){
            throw new IOException("Class patch not found." +
                    "Some other random stuff was there.", e);
        }
        catch(ClassNotFoundException e){
            throw new IOException("Class patch not found." +
                    "Some other random stuff was there.", e);
        }
        in.close();
        return cp;
    }
    
}
